﻿// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. 
// See License.txt in the project root for license information.
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Threading.Actors;
using System.Threading.Actors.Languages;
using System.Numerics;
using System.Reflection;

namespace TestActorMethods
{
    [TestClass]
    public class PythonUnitTests
    {
        [TestMethod]
        public void Python_BasicTest()
        {
            var state1 = new StandaloneActorState("actor1");
            state1.AddLanguagePack(SupportedLanguages.Python, () => new PythonLanguagePackage(state1));

            state1.Set("A", 42);

            // test basic method calls

            // Test State.Set
            state1.AddMethod(SupportedLanguages.Python, "SetKey", @"
def SetKey(state, keyname, value):
    state.Set(keyname, value)
");
            // Test State.Get
            state1.AddMethod(SupportedLanguages.Python, "GetKey", @"
def GetKey(state, keyname):
    return state.Get(keyname)
");
            // Test State.TryGet
            state1.AddMethod(SupportedLanguages.Python, "TryGetKey", @"
def TryGetKey(state, keyname):
    holder = clr.Reference[object]()
    return state.TryGet(keyname, holder)
");
            // Test State.Remove
            state1.AddMethod(SupportedLanguages.Python, "RemoveKey", @"
def RemoveKey(state, keyname):
    state.Remove(keyname)
");
            // Test State.CallMethod
            state1.AddMethod(SupportedLanguages.Python, "Call", @"
def Call(state, methodName, *args):
    return state.CallMethod(methodName, args)
");
            // Test that types defined in C# are handled properly
            state1.AddMethod(SupportedLanguages.Python, "ReturnC", @"
def ReturnC(state, keyname):
    temp = state.Get(keyname)
    return temp.C
");

            // Test Set/Get
            state1.CallMethod("SetKey", new object[] { "A", 13 });
            Assert.IsTrue((int)state1.Get("A") == 13, "Expected value of 'A' to change");

            // Test that Get of non-existent key throws exception
            Exception exc = null;
            try
            {
                int ires = (int)state1.CallMethod("GetKey", new object[] { "B" });
            }
            catch (Exception e)
            {
                exc = e;
            }
            Assert.IsNotNull(exc, "Expected exception on GetKey of non-existent key");
            Assert.IsTrue(exc is KeyNotFoundException, String.Format("Expected KeyNotFoundException after bad GetKey, got {0}", exc.GetType().Name));

            // Test TryGet
            bool exists = (bool) state1.CallMethod("TryGetKey", new object[] { "A" });
            Assert.IsTrue(exists, "Expected TryGetKey('A') to be true");
            exists = (bool)state1.CallMethod("TryGetKey", new object[] { "B" });
            Assert.IsFalse(exists, "Expected TryGetKey('B') to be false");

            // Test Remove
            state1.CallMethod("RemoveKey", new object[] { "A" });
            exists = (bool)state1.CallMethod("TryGetKey", new object[] { "A" });
            Assert.IsFalse(exists, "Expected TryGetKey('A') to be false after removal");
            object temp;
            Assert.IsFalse(state1.TryGet("A", out temp), "Expected TryGet to fail on 'A' after removal");

            
            // Test Set/Get of various types, make sure that they behave sensibly
            // There are 4 numeric types supported in Python, and here is what they translate to:
            //      int         Any integer in int range        int
            //      float       Any number with a decimal       double
            //      long        Any integer outside int range   BigInteger
            //      complex     Any number with a j after it    ??
            state1.CallMethod("SetKey", new object[] { "longKey", (long)1000000000000 });
            state1.CallMethod("SetKey", new object[] { "floatKey", (float) 103.7 });
            state1.CallMethod("SetKey", new object[] { "doubleKey", (double)3.14 });
            state1.CallMethod("SetKey", new object[] { "shortKey", (short)63 });
            UDT1 udt1 = new UDT1() { A = 42, B = 1.414, C = "Hello" };
            //state1.Set("udtKey", udt1);
            state1.CallMethod("SetKey", new object[] { "udtKey", udt1 });

            temp = state1.CallMethod("GetKey", new object[] { "longKey" });
            var longResult = (long)temp;
            temp = state1.CallMethod("GetKey", new object[] { "floatKey" });
            var floatResult = (float)temp;
            var doubleResult = (double)state1.CallMethod("GetKey", new object[] { "doubleKey" });
            var shortResult = (short)state1.CallMethod("GetKey", new object[] { "shortKey" });
            var udtResult = (UDT1)state1.CallMethod("GetKey", new object[] { "udtKey" });

            Assert.AreEqual(longResult, 1000000000000, "Expected longResult of 1000000000000");
            Assert.AreEqual(floatResult, (float) 103.7, "Expected floatResult of 103.7");
            Assert.AreEqual(doubleResult, (double) 3.14, "Expected doubleResult of 3.14");
            Assert.AreEqual(shortResult, (short) 63, "Expected shortResult of 63");
            Assert.AreEqual(udt1, udtResult, "Expected udt results to match");

            // Verify that Python can work with C#-defined user-defined types
            var s1 = (string)state1.CallMethod("ReturnC", new object[] {"udtKey"});
            Assert.AreEqual(s1, "Hello", "Expected ReturnC to return Hello for udt1");

            // Test CallMethod
            temp = state1.CallMethod("Call", new object[] { "GetKey", "doubleKey" });
            Assert.AreEqual((double)temp, 3.14, "Expected Call('GetKey', 'doubleKey') to return 3.14");
        }

        [TestMethod]
        public void Python_InteropTest()
        {
            var state1 = new StandaloneActorState("actor1");
            state1.AddLanguagePack(SupportedLanguages.Python, () => new PythonLanguagePackage(state1));

            // A C# method that creates, stores and returns a list
            state1.Set("CreateList", (Func<IActorState, object[], object>)delegate(IActorState state, object[] args)
            {
                // List name is arg 0
                var listName = (string)args[0];

                // Remaining args are elements; assume they are ints
                var numElements = args.Length - 1;
                var theList = new List<int>(numElements);
                for (int i = 1; i < args.Length; i++) theList.Add((int)args[i]);

                state.Set(listName, theList);
                return theList;
            });
            // A C# method that performs an index function on a list
            state1.Set("AtIndex", (Func<IActorState, object[], object>)delegate(IActorState state, object[] args)
            {
                var listName = (string)args[0];
                var index = (int)args[1];

                var obj = state.Get(listName);

                var ilist = obj as IList<int>;
                if (ilist != null) return ilist[index];

                var olist = obj as IList<object>;
                if (olist != null) return olist[index];

                throw new InvalidOperationException("Could not resolve the list to a known type");
            });

            // A python method that creates a list using C# method, then manipulates it and returns the result
            // "CDR" a play on Lisp's cdr mechanism.
            state1.AddMethod(SupportedLanguages.Python, "GetAt", @"
def GetAt(state, listname, index):
    theList = state.Get(listname)
    return theList[index]
");
            state1.AddMethod(SupportedLanguages.Python, "CreatePythonList", @"
def CreatePythonList(state, listname, *args):
    result = list(args)
    state.Set(listname, result)
    return result
");

            // Create a list with .NET method, then access it with Python method
            object temp;
            var original = state1.CallMethod("CreateList", new object[] { "myList", 1, 3, 5, 7, 9 });
            temp = state1.CallMethod("GetAt", new object[] { "myList", 1 });
            Assert.AreEqual((int)temp, 3, "Expected GetAt(myList, 1) to return 3");

            // Create a Python list, then access it with a .NET method
            var plist = state1.CallMethod("CreatePythonList", new object[] { "plist", 2, 4, 6, 8, 10, 12, 14, 16, 18, 20 });
            temp = state1.CallMethod("AtIndex", new object[] { "plist", 2 });
            Assert.AreEqual((int)temp, 6, "Expected AtIndex(plist,2) to return 6");

            // And mix and match
            temp = state1.CallMethod("AtIndex", new object[] { "myList", 2 });
            Assert.AreEqual((int)temp, 5, "Expected AtIndex(myList,2) to return 5");
            temp = state1.CallMethod("GetAt", new object[] { "plist", 1 });
            Assert.AreEqual((int)temp, 4, "Expected GetAt(plist, 1) to return 4");

            Console.WriteLine("Done");

        }

        [TestMethod]
        public void Python_A2ATest()
        {
            var state1 = new StandaloneActorState("actor1");
            state1.AddLanguagePack(SupportedLanguages.Python, () => new PythonLanguagePackage(state1));
            var state2 = new StandaloneActorState("actor2");
            state2.AddLanguagePack(SupportedLanguages.Python, () => new PythonLanguagePackage(state2));

            // Something for actor1 to call on actor2
            state2.Set("MethodA", (Func<IActorState, object[], object>)delegate(IActorState state, object[] args)
            {
                // Maintain a count of the number of times that you have been called
                int currCount = 0;
                object temp;
                if (state.TryGet("CallCount", out temp))
                    currCount = (int) temp;
                currCount++;
                state.Set("CallCount", currCount);

                // And return the current count
                return currCount;
            });

            // Python methods that call a 'state2' actor method
            state1.AddMethod(SupportedLanguages.Python, "CallFnF", @"
def CallFnF(state):
    proxy = state.GetActorProxy('actor2', None)
    proxy.Command('MethodA', None)
");
            state1.AddMethod(SupportedLanguages.Python, "CallSync", @"
def CallSync(state):
    proxy = state.GetActorProxy('actor2', None)
    promise = proxy.Request('MethodA', None)
    return promise.GetTypedResult[int]()
");
            state1.AddMethod(SupportedLanguages.Python, "CallAsync", @"
def CallAsync(state):
    proxy = state.GetActorProxy('actor2', None)
    promise = proxy.Request('MethodA', None)
    promise.ContinueWith('AsyncResultHandler')
    return promise
");
            state1.AddMethod(SupportedLanguages.Python, "AsyncResultHandler", @"
def AsyncResultHandler(state, promise):
    state.Set('AsyncResult', promise.GetTypedResult[int]())
");

            object obj;

            // Test fire-and-forget call
            state1.CallMethod("CallFnF", new object[0]);
            // That should complete synchronously with StandaloneActorState
            obj = state2.Get("CallCount");
            Assert.AreEqual((int)obj, 1, "Expected state2.CallCount to be 1 after FnF call");

            // Test synchronous wait call
            obj = state1.CallMethod("CallSync", new object[0]);
            Assert.AreEqual((int)obj, 2, "Expected CallSync to return 2, as it is the second call to Method1");
            obj = state2.Get("CallCount");
            Assert.AreEqual((int)obj, 2, "Expected state2.CallCount to be 2 after Sync call");

            // Test asynchronous call
            obj = state1.CallMethod("CallAsync", new object[0]);
            var promise = (IActorPromise)obj;
            Assert.IsTrue(promise.Wait(1000), "Async promise timed out");
            Assert.AreEqual(promise.GetTypedResult<int>(), 3, "Expected promise.Result to be 3 after Async call");
            obj = state2.Get("CallCount");
            Assert.AreEqual((int)obj, 3, "Expected state2.CallCount to be 3 after Async call");
            //Assert.IsTrue(state1.WaitForKey("AsyncResult", 1000), "Expected AsyncResult to be set within a second");
            //Assert.AreEqual((int)state1.Get("AsyncResult"), 3, "Expected AsyncResult to be 3");
        }

        [TestMethod]
        public void PythonA2AContinuationTest()
        {
            var state = new StandaloneActorState("me");
            state.AddLanguagePack(SupportedLanguages.Python, () => new PythonLanguagePackage(state));

            // Set up second actor/state
            var otherState = new StandaloneActorState("someOtherActor");
            otherState.AddLanguagePack(SupportedLanguages.Python, () => new PythonLanguagePackage(otherState));

            // Consider making this take a long time, to trigger any potential race by not waiting 
            // on ContinueWith's returned promise.  (Couldn't call time.sleep(1) for some reason.  Missing library?)
            state.AddMethod(SupportedLanguages.Python, "ContinuationHandler", @"
def ContinuationHandler(state, promise):
    state.Set('result', promise.GetTypedResult[int]())
    return None
");

            otherState.AddMethod(SupportedLanguages.Python, "Method1", @"
def Method1(state):
    return 42
");

            // Now from the first actor, get a proxy to the other actor and call method1 on it.
            // Call ContinueWith on an IActorPromise, then get the second IActorPromise returned by
            // ContinueWith.  Later, block on that and read the state written by the continuation.
            state.AddMethod(SupportedLanguages.Python, "CallAndRunContinuation", @"
def CallAndRunContinuation(state):
    proxy = state.GetActorProxy('someOtherActor', None)
    promise = proxy.Request('Method1', None)
    rval = promise.Result
    continuationPromise = promise.ContinueWith('ContinuationHandler')
    return continuationPromise
");

            IActorPromise continuationPromise = (IActorPromise) state.CallMethod("CallAndRunContinuation", new Object[0]);
            continuationPromise.Wait();
            // Now we can read 'result' from our IActorState safely.

            Object storedResult = null;
            if (state.TryGet("result", out storedResult))
            {
                Assert.IsTrue((int)storedResult == 42, "Expected \"result\" to be set to 42 after Python method call w/ continuation and waited on the promise.  Stored result was: " + storedResult);
            }
            else
                Assert.Fail("After calling a method and blocking on an appropriate continuation, the result wasn't set.");
        }

    }

    [Serializable]
    public class UDT1
    {
        public int A { get; set; }
        public double B { get; set; }
        public string C { get; set; }

        public override bool Equals(object obj)
        {
            UDT1 other = obj as UDT1;

            if (other == null) return false;
            return (A == other.A && B == other.B && C.Equals(other.C));
        }

        public override int GetHashCode()
        {
            return base.GetHashCode();
        }
    }


}
